package com.kordar.rbac;

import com.baomidou.dynamic.datasource.annotation.DS;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Slf4j
@DS("#kordardb")
public class DbAuthManager extends BaseManager {

    private final AuthManagerMapper mapper;

    public DbAuthManager(AuthManagerMapper authManagerMapper) {
        this.mapper = authManagerMapper;
    }


    protected boolean cache = true;

    /**
     * Item[] all auth items (name => Item)
     */
    protected Map<String, Item> items = new ConcurrentHashMap<>();

    /**
     * Rule[] all auth rules (name => Rule)
     */
    protected Map<String, Rule> rules = new ConcurrentHashMap<>();

    /**
     * array auth item parent-child relationships (childName => list of parents)
     */
    protected Map<String, Set<String>> parents = new ConcurrentHashMap<>();

    protected Map<Long, Map<String, Assignment>> _checkAccessAssignments = new ConcurrentHashMap<>();

    @Override
    protected Item getItem(String name) {
        if (name.isEmpty()) {
            return null;
        }

        if (items.containsKey(name)) {
            return items.get(name);
        }

        return mapper.getItem(name);
    }

    @Override
    protected Set<Item> getItems(Integer type) {
        return mapper.getItems(type);
    }

    @Override
    public Rule getRule(String name) {
        if (!rules.isEmpty()) {
            return rules.getOrDefault(name, null);
        }

        return mapper.getRule(name);
    }

    @Override
    public Map<String, Rule> getRules() {
        if (!rules.isEmpty()) {
            return rules;
        }

        return mapper.getRules();
    }

    protected void invalidateCache() {
        if (this.cache) {
            items.clear();
            rules.clear();
            parents.clear();
        }

        _checkAccessAssignments.clear();
    }

    protected boolean refreshInvalidateCache(boolean operator) {
        if (operator) {
            invalidateCache();
            return true;
        }
        return false;
    }

    @Override
    protected boolean addItem(Item item) {
        return refreshInvalidateCache(mapper.addItem(item));
    }

    @Override
    protected boolean addRule(Rule rule) {
        return refreshInvalidateCache(mapper.addRule(rule));
    }

    @Override
    protected boolean removeItem(Item item) {
        mapper.removeItem(item);
        invalidateCache();
        return true;
    }

    @Override
    protected boolean removeRule(Rule rule) {
        mapper.removeRule(rule);
        invalidateCache();
        return true;
    }

    @Override
    protected boolean updateItem(String name, Item item) {
        mapper.updateItem(name, item);
        return true;
    }

    @Override
    protected boolean updateRule(String name, Rule rule) {
        mapper.updateRule(name, rule);
        return true;
    }

    @Override
    public Set<Role> getRolesByUser(Long userId) {
        Set<Role> roles = new HashSet<>();
        if (userId <= 0) {
            return roles;
        }

        return mapper.findRolesByUser(userId);
    }

    @Override
    public Set<Role> getChildRoles(String roleName) {
        Role role = getRole(roleName);

        if (role == null) {
            throw new RuntimeException("Role \"" + roleName + "\" not found.");
        }

        Map<String, Boolean> result = new HashMap<>();
        getChildrenRecursive(roleName, getChildrenList(), result);

        Set<Role> roles = new HashSet<>();
        roles.add(role);

        for (Role r : getRoles()) {
            if (result.containsKey(r.getName())) {
                roles.add(r);
            }
        }

        return roles;
    }

    protected Map<String, Set<String>> getChildrenList() {
        Map<String, Set<String>> map = new HashMap<>();
        List<ItemChildren> list = mapper.findChildrenList();
        for (ItemChildren itemChildren : list) {
            Set<String> strings;
            if (map.containsKey(itemChildren.getParent())) {
                strings = map.get(itemChildren.getParent());
            } else {
                strings = new HashSet<>();
            }
            strings.add(itemChildren.getChild());
            map.put(itemChildren.getParent(), strings);
        }
        return map;
    }

    protected void getChildrenRecursive(String name, Map<String, Set<String>> childrenList, Map<String, Boolean> result) {
        if (childrenList.containsKey(name)) {
            for (String child : childrenList.get(name)) {
                result.put(child, true);
                getChildrenRecursive(child, childrenList, result);
            }
        }
    }

    @Override
    public Set<Permission> getPermissionsByRole(String roleName) {
        Map<String, Set<String>> childrenList = getChildrenList();
        Map<String, Boolean> result = new HashMap<>();
        getChildrenRecursive(roleName, childrenList, result);

        Set<Permission> permissions = new HashSet<>();
        if (result.isEmpty()) {
            return permissions;
        }

        Set<Item> list = mapper.getItemList(ItemEnum.TYPE_PERMISSION.getCode(), result.keySet());
        for (Item item : list) {
            Permission permission = new Permission();
            BeanUtils.copyProperties(item, permission, "type");
            permissions.add(permission);
        }

        return permissions;
    }

    @Override
    public Set<Permission> getPermissionsByUser(Long userId) {
        Set<Permission> permissions = getDirectPermissionsByUser(userId);
        permissions.addAll(getInheritedPermissionsByUser(userId));
        return permissions;
    }

    /**
     * Returns all permissions that are directly assigned to user.
     *
     * @param userId string|int $ the user ID (see [[\yii\web\User::id]])
     * @return Permission[] all direct permissions that the user has. The array is indexed by the permission names.
     */
    protected Set<Permission> getDirectPermissionsByUser(long userId) {
        Set<Permission> permissions = new HashSet<>();
        if (userId <= 0) {
            return permissions;
        }

        return mapper.findPermissionsByUser(userId);
    }

    protected Set<Permission> getInheritedPermissionsByUser(long userId) {
        Set<Assignment> list = mapper.findAssignmentByUser(userId);
        Map<String, Set<String>> childrenList = getChildrenList();
        Map<String, Boolean> result = new HashMap<>();
        for (Assignment assignment : list) {
            getChildrenRecursive(assignment.getItemName(), childrenList, result);
        }

        Set<Permission> permissions = new HashSet<>();
        if (result.isEmpty()) {
            return permissions;
        }

        Set<Item> setItem = mapper.getItemList(ItemEnum.TYPE_PERMISSION.getCode(), result.keySet());
        for (Item item : setItem) {
            Permission permission = new Permission();
            BeanUtils.copyProperties(item, permission, "type");
            permissions.add(permission);
        }
        return permissions;
    }

    @Override
    public boolean canAddChild(Item parent, Item child) {
        return !detectLoop(parent, child);
    }

    protected boolean detectLoop(Item parent, Item child) {
        if (child.getName().equals(parent.getName())) {
            return true;
        }
        for (Item item : getChildren(child.getName())) {
            if (detectLoop(parent, item)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public boolean addChild(Item parent, Item child) {
        if (parent.getName().equals(child.getName())) {
            throw new RuntimeException("Cannot add '" + parent.getName() + "' as a child of itself.");
        }

        if (parent instanceof Permission && child instanceof Role) {
            throw new RuntimeException("Cannot add a role as a child of a permission.");
        }

        if (detectLoop(parent, child)) {
            throw new RuntimeException("Cannot add '" + child.getName() + "' as a child of '" + parent.getName() + "'. A loop has been detected.");
        }
        mapper.addItemChild(parent.getName(), child.getName());
        invalidateCache();
        return true;
    }

    @Override
    public boolean removeChild(Item parent, Item child) {
        return mapper.removeChild(parent.getName(), child.getName());
    }

    @Override
    public boolean removeChildren(Item parent) {
        return refreshInvalidateCache(mapper.removeChildren(parent.getName()));
    }

    @Override
    public boolean hasChild(Item parent, Item child) {
        return mapper.hasChild(parent.getName(), child.getName());
    }

    @Override
    public Set<Item> getChildren(String name) {
        return mapper.findChildren(name);
    }

    @Override
    public Assignment assign(Item item, Long userId) {
        mapper.assign(item.getName(), userId, new Date());
        _checkAccessAssignments.remove(userId);
        return new Assignment(userId, item.getName(), new Date());
    }

    @Override
    public boolean revoke(Item item, Long userId) {
        if (userId <= 0) {
            return false;
        }

        _checkAccessAssignments.remove(userId);

        return mapper.removeAssignment(item.getName(), userId);
    }

    @Override
    public boolean revokeAll(Long userId) {
        if (userId <= 0) {
            return false;
        }

        _checkAccessAssignments.remove(userId);

        return mapper.removeAllAssignmentByUser(userId);
    }

    @Override
    public Assignment getAssignment(String roleName, Long userId) {
        if (userId <= 0) {
            return null;
        }

        return mapper.getAssignment(roleName, userId);
    }

    @Override
    public Map<String, Assignment> getAssignments(Long userId) {
        Map<String, Assignment> map = new HashMap<>();
        if (userId <= 0) {
            return map;
        }

        return mapper.getAssignments(userId);
    }

    @Override
    public void removeAllAssignments() {
        _checkAccessAssignments.clear();
        mapper.removeAllAssignments();
    }

    @Override
    public Set<Long> getUserIdsByRole(String roleName) {
        Set<Long> list = new HashSet<>();
        if (!StringUtils.hasText(roleName)) {
            return list;
        }

        return mapper.getAllAssignment().stream()
                .map(Assignment::getUserId).collect(Collectors.toSet());
    }

    @Override
    public void removeAll() {
        mapper.removeAll();
        invalidateCache();
    }

    protected void removeAllItems(Integer type) {

        Set<Item> items = mapper.getItems(type);
        if (items.isEmpty()) {
            return;
        }

        Set<String> names = items.stream().map(Item::getName).collect(Collectors.toSet());
        String key = type.equals(ItemEnum.TYPE_PERMISSION.getCode()) ? "child" : "parent";
        mapper.removeChildByNames(key, names);
        mapper.removeAssignmentByName(names);
        mapper.removeItemByType(type);
        invalidateCache();
    }

    @Override
    public void removeAllPermissions() {
        removeAllItems(ItemEnum.TYPE_PERMISSION.getCode());
    }

    @Override
    public void removeAllRoles() {
        removeAllItems(ItemEnum.TYPE_ROLE.getCode());
    }

    @Override
    public void removeAllRules() {
        mapper.removeAllRules();
        invalidateCache();
    }

    @Override
    public boolean checkAccess(Long userId, String permissionName, Map<String, Object> params) {
        Map<String, Assignment> assignments;
        if (_checkAccessAssignments.containsKey(userId)) {
            assignments = this._checkAccessAssignments.get(userId);
        } else {
            assignments = this.getAssignments(userId);
            this._checkAccessAssignments.put(userId, assignments);
        }

        if (hasNoAssignments(assignments)) {
            return false;
        }

        loadFromCache();

        if (this.items != null) {
            return checkAccessFromCache(userId, permissionName, params, assignments);
        }

        return this.checkAccessRecursive(userId, permissionName, params, assignments);
    }

    public void loadFromCache() {
        if (!this.items.isEmpty() || !cache) {
            return;
        }

        invalidateCache();

        mapper.findAllItems().forEach(e -> this.items.put(e.getName(), e));
        this.rules = mapper.getRules();
        List<ItemChildren> itemChildren = mapper.findChildrenList();
        for (ItemChildren itemChild : itemChildren) {
            if (this.items.containsKey(itemChild.getChild())) {
                Set<String> set = null;
                if (this.parents.containsKey(itemChild.getChild())) {
                    set = this.parents.get(itemChild.getChild());
                } else {
                    set = new HashSet<>();
                }
                set.add(itemChild.getParent());
                this.parents.put(itemChild.getChild(), set);
            }
        }
    }

    /**
     * Performs access check for the specified user based on the data loaded from cache.
     * This method is internally called by [[checkAccess()]] when [[cache]] is enabled.
     *
     * @param user        string|int the user ID. This should can be either an integer or a string representing
     *                    the unique identifier of a user. See [[\yii\web\User::id]].
     * @param itemName    string the name of the operation that need access check
     * @param params      array name-value pairs that would be passed to rules associated
     *                    with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
     *                    which holds the value of `$userId`.
     * @param assignments Assignment[] the assignments to the specified user
     * @return bool whether the operations can be performed by the user.
     * @since 2.0.3
     */
    protected boolean checkAccessFromCache(Long user, String itemName, Map<String, Object> params, Map<String, Assignment> assignments) {
        if (!this.items.containsKey(itemName)) {
            return false;
        }

        Item item = this.items.get(itemName);
        System.out.println(item);

        log.debug(item instanceof Role ? "Checking role: " + itemName : "Checking permission: " + itemName);

        if (!this.executeRule(user, item, params)) {
            return false;
        }

        if (assignments.containsKey(itemName) || defaultRoles.containsKey(itemName)) {
            return true;
        }

        if (!this.parents.isEmpty()) {
            for (String parent : this.parents.get(itemName)) {
                if (checkAccessFromCache(user, parent, params, assignments)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Performs access check for the specified user.
     * This method is internally called by [[checkAccess()]].
     *
     * @param user        string|int $ the user ID. This should can be either an integer or a string representing
     *                    the unique identifier of a user. See [[\yii\web\User::id]].
     * @param itemName    string $ the name of the operation that need access check
     * @param params      array $ name-value pairs that would be passed to rules associated
     *                    with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
     *                    which holds the value of `$userId`.
     * @param assignments Assignment[] $ the assignments to the specified user
     * @return bool whether the operations can be performed by the user.
     */
    protected boolean checkAccessRecursive(Long user, String itemName, Map<String, Object> params, Map<String, Assignment> assignments) {
        if (!this.items.containsKey(itemName)) {
            return false;
        }

        Item item = this.items.get(itemName);
        log.debug(item instanceof Role ? "Checking role: " + itemName : "Checking permission: " + itemName);

        if (!executeRule(user, item, params)) {
            return false;
        }

        if (assignments.containsKey(itemName) || defaultRoles.containsKey(itemName)) {
            return true;
        }

        List<ItemChildren> list = mapper.findChildrenFormChild(itemName);
        for (ItemChildren itemChild : list) {
            if (this.checkAccessRecursive(user, itemChild.getParent(), params, assignments)) {
                return true;
            }
        }

        return false;
    }

}
